Skip to content

Conversation

@avdudchenko
Copy link
Contributor

Summary/Motivation:

Currently, we spend alot of time building custom flowsheets that require full re-parametrization of each unit model. This makes the units non-portable, and results in duplicate code. We also use low-level API for building flowsheets, where all units have to be manually connected via arcs, and propagated. This can make sense in a few scenarios (Where a flowsheet has a very complex connection structure), but in most cases, the connections and their flow is mostly linear (even in complex processes like LSRRO).

The WaterTAPFlowsheetBlock brings a layer of abstraction for units with focus on providing a standard building platform for flowsheets where unit models become portable between flowsheets, and where time spent on configuring each unit model is minimized. It defines standard functions necessary to fully define a unit model from fixed operation variables to scaling factors, and additionally provides a method for standard connection management. A full example of benefits/implementation can be found in this branch and flowsheet

An example workflow with unit models build on watertapflowsheet block may look as follows:

# Define our flowsheet and properties
m.fs = FlowsheetBlock()
m.fs.costing = WaterTAPCosting()
m.fs.properties = MCASParameterBlock(**mcas_props)

# Watertap flowsheet units 
# Here feed specs would be a dict that contains information about feed composition.
m.fs.feed = MultiCompFeed(
        default_property_package=m.fs.properties,
        **feed_specs,
    )

#Here softening specs would be a dict that defines reactions and solids that would form
m.fs.softening_unit = PrecipitationUnit(
    default_property_package=m.fs.properties,
    default_costing_package=m.fs.costing,
    **softeining_specs)

# standard IDAES units
m.fs.product= Product(property_package=m.fs.seawater_props)
m.fs.solids_waste =  Product(property_package=m.fs.seawater_props)

# this builds all the arcs for us and adds tracking for thier propagation
m.fs.feed.outlet.connect_to(m.fs_softening_unit.inlet)
m.fs.fs_softening_unit.treated.connect_to(m.product.inlet) # connects to normal IDAES units!
m.fs.fs_softening_unit.waste.connect_to(m.solids_waste.inlet) # connects to normal IDAES units!
# standard arc expansion!
TransformationFactory("network.expand_arcs").apply_to(m)

m.fs.feed.fix_and_scale() # fixes operation and scales the unit model

m.fs.feed.initialize() # intialzies and propagates all outlet connections
m.fs.softening_unit.initialzie() #initlaizes and propages all outlet connections

# the outlets from softening were propagated by the WaterTAP flowsheet block, so we can just initialize these!
m.fs.product.initialize()
m.fs.solid_waste.initalize()

... do a box solve if wanted 

# use built-in optimization config function to do our optimization. 
m.fs.softening.set_optimization_operation()

.... go on to do optimization etc. 

Changes proposed in this PR:

  • Add watertapflowsheet block
  • Add connection utilities that manage arcs and equality constraints for unit connections
  • Add reporting utility for building custom reports for flowsheet units.

Legal Acknowledgement

By contributing to this software project, I agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the license terms described in the LICENSE.txt file at the top level of this directory.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

@avdudchenko avdudchenko marked this pull request as ready for review November 11, 2025 23:01
@ksbeattie ksbeattie added the Priority:High High Priority Issue or PR label Nov 13, 2025
def build(self):
super().build()
self.feed = Feed(property_package=self.config.default_property_package)
self.solute_type = list(self.config.default_property_package.solute_set)[0]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have examples of this for other property models?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work for nay property model, the implementation here assume a single solute, but can be extended to work with MCAS as well. (I know it works with Seawater, NaCl and MCAS prop pacakges at least..).

class WaterTapFlowsheetBlockData(FlowsheetBlockData):
CONFIG = FlowsheetBlockData.CONFIG()
CONFIG.declare(
"default_costing_package",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing you adopted this prepending of default_ nomenclature from FlowsheetBlock config, do you know why they do this? It is strange. Like, why not just costing_package or property_package. There could be still be defaults.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The based on idaes flowsheet block. The idea is that you can have more then one property block and maybe even costing blocks on a flowsheet model.

Example is using RO with MCAS, the default property package is "MCAS" and ro_property_pacakges is NaCl prop pack or SeawaterPropPack, etc. Its just very explicit in the supplied prop pack is "default" acting prop pack that this flowsheet unit can work with.

@kurbansitterley
Copy link
Contributor

I would be interested in seeing other more complex unit implementations if that is somewhere

@avdudchenko
Copy link
Contributor Author

avdudchenko commented Nov 24, 2025

I would be interested in seeing other more complex unit implementations if that is somewhere

@kurbansitterley If you read the PR text, I link directly to reaktoro enabled flowsheet examples that use this stucture...(You should also have access to hpro_analysis repo which is private if anyone else try to acces (ping me on slack and I can invite you)).

Comment on lines +76 to +89
def set_fixed_operation(self, **kwargs):
"""Developer should implement a routine to fix unit operation for initialization and 0DOF solving"""

def set_optimization_operation(self, **kwargs):
"""Developer should implement a routine to unfix variables for unit to perform optimization"""

def scale_before_initialization(self, **kwargs):
"""Developer should implement scaling function to scale unit using
default values before initialization routine is ran"""

def scale_post_initialization(self, **kwargs):
"""Developer should implement scaling function to scale unit after initialization routine is ran"""

def register_port(self, name, port=None, var_list=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we enforce that any of these be developed if subclassing from this? If so, which ones should be deemed "required"? The reason to do so would really be to help the user to make sure to implement methods that provide the most value and make life easier.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ANd this question goes for any of the methods that are placeholders right now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority:High High Priority Issue or PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants